﻿using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using GalaSoft.MvvmLight.Command;
using io = System.IO;
using InstantSpine.Core;
using InstantSpine.GUI.Properties;
using InstantSpine.Core.ParaSpine;

namespace InstantSpine.GUI
{
    sealed class MainWindowViewModel : INotifyPropertyChanged
    {
        /// <summary>
        /// Indicates whether a property has changed its value
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        string loadedFile;
        /// <summary>
        /// The currently loaded model config
        /// </summary>
        public string LoadedFile {
            get { return loadedFile; }
            set {
                if (loadedFile != value) {
                    loadedFile = value;
                    // WindowTitle depends on this value
                    PropertyChanged.Raise(() => WindowTitle);
                }
            }
        }

        /// <summary>
        /// String to be displayed as the window’s title
        /// </summary>
        public string WindowTitle {
            get { return "InstantSpine - " + io.Path.GetFileName(LoadedFile); }
        }

        /// <summary>
        /// The name of a Python script file that contains a function "update_parameters"
        /// that updates the anatomy parameters of a provided VertebraDiscAnatomy instance
        /// </summary>
        public string RegressionScriptFileName {
            get { return SpineModel.Anatomy.ScriptFileName; }
            set {
                if (value != SpineModel.Anatomy.ScriptFileName) {
                    SpineModel.Anatomy.ScriptFileName = value;

                    if (!string.IsNullOrEmpty(SpineModel.Anatomy.ScriptFileName) &&
                        System.IO.File.Exists(SpineModel.Anatomy.ScriptFileName.Trim().Trim('"'))) {
                        CanEnableRegression = true;
                        SpineModel.Anatomy.ScriptFileName = SpineModel.Anatomy.ScriptFileName;
                        if (SpineModel.Anatomy.UseScript == true) {
                            CompileAndEnableParameterComputationAsync();
                        }
                    }
                    else {
                        CanEnableRegression = false;
                        UseRegression = false;
                        DisableParameterComputation();
                    }

                    PropertyChanged.Raise(this);
                    Debug.WriteLine("Script file = " + SpineModel.Anatomy.ScriptFileName);
                }
            }
        }

        private bool canEnableRegression;
        /// <summary>
        /// Determines whether computed parameters can
        /// be enabled or not, i.e. whether UseRegression can
        /// be set to true
        /// </summary>
        public bool CanEnableRegression {
            get { return canEnableRegression; }
            set {
                if (canEnableRegression != value) {
                    canEnableRegression = value;
                    PropertyChanged.Raise(this);
                }
            }
        }

        /// <summary>
        /// Determines whether to automatically compute
        /// parameters given ParameterComputer
        /// </summary>
        public bool UseRegression {
            get { return SpineModel.Anatomy.UseScript; }
            set {
                if (value != SpineModel.Anatomy.UseScript) {
                    SpineModel.Anatomy.UseScript = value;
                    
                    // => enabled the use of regression equations
                    // => load script + bind rules
                    if (canEnableRegression && SpineModel.Anatomy.UseScript) {
                        CompileAndEnableParameterComputationAsync();
                    }
                    else {
                        DisableParameterComputation();
                    }

                    PropertyChanged.Raise(this);
                }
            }
        }

        private bool isBusyComputingAnatomy;
        /// <summary>
        /// Determines whether changing the anatomy data should
        /// be possible or not
        /// </summary>
        public bool IsBusyComputingAnatomy {
            get { return isBusyComputingAnatomy; }
            set {
                if (value != isBusyComputingAnatomy) {
                    isBusyComputingAnatomy = value;
                    PropertyChanged.Raise(this);
                }
            }
        }

        private bool isBusyGeneratingModel;
        /// <summary>
        /// Indicates that InstantSpine is busy generating
        /// an FE model based on the given parameterization
        /// </summary>
        public bool IsBusyGeneratingModel {
            get { return isBusyGeneratingModel; }
            set {
                if (value != isBusyGeneratingModel) {
                    isBusyGeneratingModel = value;
                    PropertyChanged.Raise(this);
                }
            }
        }

        private string anatomyBusyMessage;
        /// <summary>
        /// Indicates the current reason for showing a busy indicator
        /// preventing interaction with the anatomy data pane
        /// </summary>
        public string AnatomyBusyMessage {
            get { return anatomyBusyMessage; }
            set {
                if (value != anatomyBusyMessage) {
                    anatomyBusyMessage = value;
                    PropertyChanged.Raise(this);
                }
            }
        }

        private string modelGenerationBusyMessage;
        /// <summary>
        /// Indicates the current reason for showing a busy indicator
        /// preventing interaction with any data pane or the main menu
        /// </summary>
        public string ModelGenerationBusyMessage {
            get { return modelGenerationBusyMessage; }
            set {
                if (value != modelGenerationBusyMessage) {
                    modelGenerationBusyMessage = value;
                    PropertyChanged.Raise(this);
                }
            }
        }

        private bool canGenerateModel;
        /// <summary>
        /// Determines whether an FE model can be generated
        /// based on the given data and options
        /// </summary>
        public bool CanGenerateModel {
            get { return canGenerateModel; }
            set {
                if (canGenerateModel != value) {
                    canGenerateModel = value;
                    PropertyChanged.Raise(this);
                }
            }
        }

        SpineModel spineModel;
        /// <summary>
        /// Currently displayed SpineModel instance
        /// </summary>
        public SpineModel SpineModel {
            get { return spineModel; }
            set {
                if (spineModel != value) {
                    spineModel = value;
                    PropertyChanged.Raise(this);
                }
            }
        }

        /// <summary>
        /// Behavior for the "Browse..." button which allows selecting a Python 
        /// script containing rules about how to compute anatomy parameters
        /// </summary>
        public ICommand BrowseForScriptCommand { get; private set; }

        /// <summary>
        /// Display a dialog window for setting app-wide preferences
        /// </summary>
        public ICommand DisplayOptionsDialogCommand { get; private set; }

        /// <summary>
        /// Display a save file dialog to save the current model parameterization
        /// </summary>
        public ICommand DisplaySaveModelParameterizationDialogCommand { get; private set; }

        /// <summary>
        /// Try to directly save to existing file
        /// </summary>
        public ICommand SaveModelParameterizationCommand { get; private set; }

        /// <summary>
        /// Display an open file dialog for opening an existing model spec
        /// </summary>
        public ICommand LoadModelParameterizationCommand { get; private set; }

        /// <summary>
        /// Reset the UI to default values
        /// </summary>
        public ICommand ClearAllCommand { get; private set; }

        /// <summary>
        /// Exit the application
        /// </summary>
        public ICommand ExitCommand { get; private set; }

        /// <summary>
        /// Handle Alt+F4 or system closing event
        /// </summary>
        public ICommand HandleClosingCommand { get; private set; }

        /// <summary>
        /// Display an export to csv dialog for anatomy data
        /// </summary>
        public ICommand ExportAnatomyDataCommand { get; private set; }

        /// <summary>
        /// Display an import from csv dialog for anatomy data
        /// </summary>
        public ICommand ImportAnatomyDataCommand { get; private set; }

        /// <summary>
        /// Generate a new spine model based on the chosen parameterization
        /// for the chosen FE solver
        /// </summary>
        public ICommand GenerateSpineModelCommand { get; private set; }

        //public ImageSource SpineBW { get; private set; }

        void InitDefaultModelData() {
            SpineModel = new SpineModel();
            SpineModel.Anatomy.AddDefaultAnatomyData();
            LoadedFile = Resources.NewFile;
            IsBusyComputingAnatomy = false;
            AnatomyBusyMessage = Properties.Resources.InitPythonRT;
        }

        public MainWindowViewModel() {
            InitDefaultModelData();
            
            // convert GDI+ bmp ressources to WPF ImageSource
            //SpineBWImg = Resources.SpineSeiteBW.GdiToWpfBitmap();
           
            // Commands
            BrowseForScriptCommand = new RelayCommand(ShowScriptFileSelectionDialog);
            DisplayOptionsDialogCommand = new RelayCommand(DisplayOptionsDialog);
            DisplaySaveModelParameterizationDialogCommand = new RelayCommand(DisplaySaveModelDialog, () => !isBusyComputingAnatomy && !isBusyGeneratingModel);
            SaveModelParameterizationCommand = new RelayCommand(SaveModel, () => !isBusyComputingAnatomy && !isBusyGeneratingModel);
            LoadModelParameterizationCommand = new RelayCommand(LoadModel, () => !isBusyComputingAnatomy && !isBusyGeneratingModel);
            ExportAnatomyDataCommand  = new RelayCommand(DisplayAnatomyExportDialog, () => !isBusyComputingAnatomy && !isBusyGeneratingModel);
            ImportAnatomyDataCommand  = new RelayCommand(DisplayAnatomyImportDialog, () => !isBusyComputingAnatomy && !isBusyGeneratingModel);
            GenerateSpineModelCommand = new RelayCommand(AskFEModelFileDestination,  () => !isBusyComputingAnatomy);

            ClearAllCommand = new RelayCommand(() => {
                var userChoice = AskSaveChanges("Clear all data");
                switch (userChoice) {
                    case MessageBoxResult.Cancel:
                        return;
                    case MessageBoxResult.Yes:
                        SaveModel();
                        InitDefaultModelData();
                        return;
                    case MessageBoxResult.No:
                        InitDefaultModelData();
                        return;
                    default:
                        throw new InvalidOperationException();
                }
            });

            ExitCommand = new RelayCommand(() => {
                var userChoice = AskSaveChanges();
                switch (userChoice) {
                    // cancel app exit
                    case MessageBoxResult.Cancel: 
                        return;
                    // save, then exit app
                    case MessageBoxResult.Yes: 
                        SaveModel(); 
                        Application.Current.Shutdown(); 
                        return;
                    // exit immediately
                    case MessageBoxResult.No: 
                        Application.Current.Shutdown(); 
                        return;
                    // unexpected MessageBoxResult
                    default: 
                        throw new InvalidOperationException();
                }
            });

            HandleClosingCommand = new RelayCommand<CancelEventArgs>(cea => {
#if PUBLISH
                var userChoice = AskSaveChanges();
                switch (userChoice) {
                    // cancel closing
                    case MessageBoxResult.Cancel: 
                        cea.Cancel = true; 
                        return;
                    // save, then exit
                    case MessageBoxResult.Yes: 
                        SaveModel(); 
                        return;
                    // exit immediatly
                    case MessageBoxResult.No: 
                        return;
                    // unexpected MessageBoxResult
                    default: 
                        throw new InvalidOperationException();
                }
#endif
            });
        }

        MessageBoxResult AskSaveChanges(string msgBoxTitle = "Exit InstantSpine") {            
            /*var td = new Microsoft.WindowsAPICodePack.Dialogs.TaskDialog();
            td.Caption = "Caption";
            td.DetailsCollapsedLabel = "Collapsed Details Label";
            td.DetailsExpandedLabel = "Expanded Details Label";
            td.DetailsExpandedText = "Expanded Details: lots and lots of details about this message.";
            td.FooterCheckBoxText = "Footer Checkbox Text";
            td.FooterIcon = Microsoft.WindowsAPICodePack.Dialogs.TaskDialogStandardIcon.Information;            
            td.FooterText = "Footer Text";
            td.InstructionText = "Dies ist der Instruction Text. Dies ist der Instruction Text.";
            td.ProgressBar = new Microsoft.WindowsAPICodePack.Dialogs.TaskDialogProgressBar() { State = Microsoft.WindowsAPICodePack.Dialogs.TaskDialogProgressBarState.Marquee };
            td.StandardButtons = Microsoft.WindowsAPICodePack.Dialogs.TaskDialogStandardButtons.Yes | Microsoft.WindowsAPICodePack.Dialogs.TaskDialogStandardButtons.No;            
            td.Text = "Ein text!!";            
            td.Icon = Microsoft.WindowsAPICodePack.Dialogs.TaskDialogStandardIcon.Shield;
            td.Show();*/

            return MessageBox.Show(
                "Save current parameter set to file?", msgBoxTitle, 
                MessageBoxButton.YesNoCancel, MessageBoxImage.Question);            
        }

        /// <summary>
        /// Display a file open dialog, which allows selecting a Python script
        /// that contains the necessary logic to compute parameters when
        /// UseRegression is set to true
        /// </summary>
        void ShowScriptFileSelectionDialog() {
            var fileOpenDialog = new Microsoft.Win32.OpenFileDialog();
            fileOpenDialog.Title = "Load Python script";
            fileOpenDialog.AddExtension = true;
            fileOpenDialog.CheckFileExists = true;
            fileOpenDialog.CheckPathExists = true;
            fileOpenDialog.DefaultExt = ".py";
            fileOpenDialog.Filter = "Python scripts|*.py";
            var result = fileOpenDialog.ShowDialog();

            if (result.HasValue && result.Value && !string.IsNullOrEmpty(fileOpenDialog.FileName)) {
                RegressionScriptFileName = fileOpenDialog.FileName;
            }
        }

        /// <summary>
        /// Display the options dialog for global app settings
        /// </summary>
        void DisplayOptionsDialog() {
            var dlg = new OptionsDialog();
            // save current settings so they can be restored if the user presses cancel
            Settings.Default.Save();
            var updateSettings = dlg.ShowDialog().Value;
            if (updateSettings) {
                // OK -> save new settings
                Settings.Default.Save();
            }
            else {
                // Cancel -> restore old settings
                Settings.Default.Reload();
            }
        }

        /// <summary>
        /// Display the save as dialog
        /// </summary>
        void DisplaySaveModelDialog() {
            var fileSaveDialog = new Microsoft.Win32.SaveFileDialog();
            fileSaveDialog.Title = "Save as";
            fileSaveDialog.AddExtension = true;
            fileSaveDialog.CheckPathExists = true;
            fileSaveDialog.DefaultExt = ".xml";
            fileSaveDialog.Filter = "InstantSpine model spec|*.xism";
            var save = fileSaveDialog.ShowDialog();

            if (save.Value) {
                SaveParametersetAsync(fileSaveDialog.FileName);                
                LoadedFile = fileSaveDialog.FileName;
            }            
        }

        /// <summary>
        /// Either save the model config to an existing file
        /// or to a new file (if not yet saved)
        /// </summary>
        void SaveModel() {
            // config hasn't been saved yet
            if (LoadedFile == "New") {
                DisplaySaveModelDialog();
            }
            else {
                SaveParametersetAsync(LoadedFile);
            }
        }

        async void SaveParametersetAsync(string filename) {
            IsBusyGeneratingModel = true;
            ModelGenerationBusyMessage = string.Format("Saving model parameters to {0}", io.Path.GetFileName(filename));
            //await Task.Run(() => System.Threading.Thread.Sleep(1000));
            await Task.Run(() => SpineModel.SaveToXml(filename));
            IsBusyGeneratingModel = false;
        }

        /// <summary>
        /// Display the load from file dialog for loading model parameterizations
        /// </summary>
        /// <remarks>If computed parameters were active when the model parameterization
        /// was saved, this method enables it as well</remarks>
        void LoadModel() {
            var fileOpenDialog = new Microsoft.Win32.OpenFileDialog();
            fileOpenDialog.AddExtension = true;
            fileOpenDialog.CheckFileExists = true;
            fileOpenDialog.CheckPathExists = true;
            fileOpenDialog.DefaultExt = ".xism";
            fileOpenDialog.Filter = "InstantSpine model spec|*.xism";
            var load = fileOpenDialog.ShowDialog().Value;

            if (load) {
                SpineModel = SpineModel.LoadFromXml(fileOpenDialog.FileName);
                LoadedFile = fileOpenDialog.FileName;

                // force refresh UI
                PropertyChanged.Raise(() => RegressionScriptFileName);
                PropertyChanged.Raise(() => UseRegression);

                // activate parameter computation, if applicable
                if (!string.IsNullOrEmpty(RegressionScriptFileName) &&
                    System.IO.File.Exists(RegressionScriptFileName.Trim().Trim('"'))) {
                    CanEnableRegression = true;
                }
                if (CanEnableRegression && UseRegression) {
                    CompileAndEnableParameterComputationAsync();
                }
            }
        }

        /// <summary>
        /// Displays a dialog to export the anatomy data to CSV files
        /// </summary>
        void DisplayAnatomyExportDialog() {
            var fileSaveDialog = new Microsoft.Win32.SaveFileDialog();
            fileSaveDialog.Title = "Export anatomy data";
            fileSaveDialog.AddExtension = true;
            fileSaveDialog.CheckPathExists = true;
            fileSaveDialog.DefaultExt = ".csv";
            fileSaveDialog.Filter = "Comma separated values (CSV)|*.csv";
            var save = fileSaveDialog.ShowDialog();

            if (save.Value) {
                SpineModel.Anatomy.SaveAsCSV(fileSaveDialog.FileName);
            }
        }

        /// <summary>
        /// Displays a dialog to import anatomy data from CSV files
        /// </summary>
        /// <remarks>The CSV file must use tabulators as a separator</remarks>
        void DisplayAnatomyImportDialog() {
            var fileOpenDialog = new Microsoft.Win32.OpenFileDialog();
            fileOpenDialog.Title = "Import anatomy data";
            fileOpenDialog.AddExtension = true;
            fileOpenDialog.CheckFileExists = true;
            fileOpenDialog.CheckPathExists = true;
            fileOpenDialog.DefaultExt = ".csv";
            fileOpenDialog.Filter = "Comma separated values (CSV)|*.csv";
            var result = fileOpenDialog.ShowDialog();

            if (result.HasValue && result.Value && !string.IsNullOrEmpty(fileOpenDialog.FileName)) {
                SpineModel.Anatomy.LoadFromCSV(fileOpenDialog.FileName);
            }
        }

        private void AskFEModelFileDestination() {
            var fileSaveDialog = new Microsoft.Win32.SaveFileDialog();
            fileSaveDialog.Title = "Generate FE model";
            fileSaveDialog.AddExtension = true;
            fileSaveDialog.CheckPathExists = true;
            fileSaveDialog.DefaultExt = ".inp";
            fileSaveDialog.Filter = "Abaqus input deck (INP)|*.inp";
            var save = fileSaveDialog.ShowDialog();

            if (save.Value) {
                GenerateSpineModel(fileSaveDialog.FileName);
            }
        }        

        /// <summary>
        /// Generate an FE model of the spine based on the
        /// chosen model parameters by calling Ansys, ParaSpine etc.
        /// and converting the output to the target FE input format
        /// </summary>
        public async void GenerateSpineModel(string modelFilesDestination) {
            // TODO: check basic validity of given paramters, e.g. if all required model params have been set
            //       (i.e. check for null for nullable params)
            
            IsBusyGeneratingModel = true;
            ModelGenerationBusyMessage = Resources.BusyGeneratingModel;

            try {
                await Task.Run(() => {
                    var paraSpine = new ApdlParaSpine(
                        Settings.Default.ansysProductString,
                        Settings.Default.pathToAnsys,
                        Settings.Default.pathToParaSpine);

                    paraSpine.GenerateFEModel(SpineModel, modelFilesDestination, setStatus: (stat, currentStep, totalSteps) => {
                        var stepmofn = string.Format(Resources.StepMofN, currentStep, totalSteps);
                        // Display appropriate status message
                        switch (stat) {
                            case ModelGenerationPhase.ExportingParameterization:
                                ModelGenerationBusyMessage = stepmofn + " " + Resources.ExportingModelParameterization;
                                return;
                            case ModelGenerationPhase.GeneratingFEModel:
                                ModelGenerationBusyMessage = stepmofn + " " + Resources.GeneratingFEModel;
                                return;
                            case ModelGenerationPhase.ConvertingToTargetFormat:
                                ModelGenerationBusyMessage = stepmofn + " " + Resources.ConvertingToTargetFormat;
                                return;
                            default:
                                ModelGenerationBusyMessage = Resources.BusyGeneratingModel;
                                return;
                        }
                    });
                });
            }
            // TODO: Handle other specific exceptions thrown by ApdlParaSpine.CallPreprocessor
            //       - ParaSpineStartupScriptNotFoundException
            //       - AnsysNotFoundException
            //       - InvalidAnsysProductStringException
            //       - AnsysAbnormalExitException
            //       - ParaSpineModelGenerationException
            catch (ParaSpineNotFoundException psnfx) {
                IsBusyGeneratingModel = false;
                MessageBox.Show(
                    "ParaSpine implementation not found.\n\rCheck whether the supplied path is valid.\r\n\t" + psnfx.Message,
                    "ParaSpine not found");
            }
            catch (ParaSpineNotWriteableException) {
                IsBusyGeneratingModel = false;
                MessageBox.Show(
                    "ParaSpine directory is not writeable.\n\rCheck weather you got write permissions for the specified directory.",
                    "ParaSpine directory is read-only");
            }
            catch (io.IOException iox) {
                IsBusyGeneratingModel = false;
                // TODO: refine error message
                MessageBox.Show(iox.Message, "Generic I/O error");
            }
            finally {
                IsBusyGeneratingModel = false;
            }
        }        

        /// <summary>
        /// Load and compile a user-provided Python script to
        /// enable automatic computation of dependent anatomy
        /// parameters
        /// </summary>
        /// <remarks>
        /// The script must provide a function called update_parameters that takes
        /// an instance of type VertebraDiscAnatomy as its sole parameter and
        /// refreshes the anatomy properties of the given instance.
        /// 
        /// To avoid freezing the UI during the initialization of the IronPython
        /// runtime, this method runs asynchronously. A busy indicator is displayed
        /// during initialization and compilation and the user cannot interact with
        /// the anatomy pane.
        /// </remarks>
        private async void CompileAndEnableParameterComputationAsync() {
            // disable checkbox for enabling/disabling param. computation
            // (otherwise: user could disable it again while InstantSpine
            // is busy initializing and compiling the script -> inconsistent)
            CanEnableRegression = false;
            
            // show busy indicator while init + compiling
            IsBusyComputingAnatomy = true;
            AnatomyBusyMessage = Properties.Resources.InitPythonRT;

            SpineModel.Anatomy.ScriptFileName = SpineModel.Anatomy.ScriptFileName ?? "";
            var scriptfilename = RegressionScriptFileName.Trim().Trim('"');

            // should never happen, as set_RegressionScriptFileName
            // checks for existence itself
            if (!System.IO.File.Exists(scriptfilename)) {
                Debug.WriteLine("Script file not found");
                UseRegression = false;
                return;
            }

            try {
                await Task.Run(() => SpineModel.Anatomy.LoadAndCompileScript(
                    scriptfilename,
                    s => this.AnatomyBusyMessage = s));
            }
            catch (CompilationException cex) {
                IsBusyComputingAnatomy = false;
                // script is faulty
                UseRegression = false;

                MessageBox.Show(
                    cex.ErrorMessage,
                    cex.ErrorCaption,
                    MessageBoxButton.OK, MessageBoxImage.Error);
            }
            finally {
                IsBusyComputingAnatomy = false;
                // re-enabled checkbox for enabling/disabling param. computation
                CanEnableRegression = true;
            }
        }

        private void DisableParameterComputation() {
            SpineModel.Anatomy.DisableParameterComputation();
        }
    }
}
